package com.guonl.mybatis.interceptor;

import com.guonl.config.yaml.MysqlQueryColumnsProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;

import javax.annotation.Resource;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
@Intercepts({
        @Signature(type = Executor.class,
                method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ReplaceQueryColumnInterceptor implements Interceptor {

    @Resource
    private MysqlQueryColumnsProperties queryColumnsProperties;
    private static final String CAT_NAME = "phone";
    private static final String COLUMN_SUFFIX = "_hash_code";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement mappedStatement = (MappedStatement) args[0];
        Object parameter = args[1];
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();

        String boundSqlSql = boundSql.getSql();
//        String mainTableName = extractMainTableName(boundSqlSql);
        Map<String, List<String>> queryEncryptColumnsMap = queryColumnsProperties.getQueryTablesColumnsMap();

        List<String> allTableNameList = new ArrayList<>();
        String finalBoundSqlSql = boundSqlSql;
        queryEncryptColumnsMap.forEach((k, v) -> {
            if (finalBoundSqlSql.contains(k)) {
                allTableNameList.add(k);
            }
        });

        if (allTableNameList.size() == 0) {
            return invocation.proceed();
        }

        Set<String> allQueryColumns = getAllQueryColumns(allTableNameList, queryEncryptColumnsMap);
        if (allQueryColumns == null || allQueryColumns.size() == 0) {
            return invocation.proceed();
        }

//        boundSqlSql = boundSqlSql.replaceAll("\\R", " ");
        boundSqlSql = boundSqlSql.replaceAll("(?i)\\bwhere\\b", " WHERE ");
        boundSqlSql = boundSqlSql.replaceAll("(?i)\\bin\\b", " IN ");

        int whereIndex = boundSqlSql.lastIndexOf("WHERE");
        if (whereIndex <= 0) {
            return invocation.proceed();
        }

        Set<Integer> allIndexSet = new HashSet<>();
        if (boundSqlSql.contains(" IN ")) {
            for (String column : allQueryColumns) {
                Set<Integer> inSqlIndex = getInParameterSqlIndex(boundSqlSql, column);
                allIndexSet.addAll(inSqlIndex);
            }
        }
        String afterWhereSql = boundSqlSql.substring(whereIndex, boundSqlSql.length());
        findOtherParameterIndex(afterWhereSql, allQueryColumns, allIndexSet);

        boolean flag = false;
        for (String column : allQueryColumns) {
            if (afterWhereSql.contains(column)) {
                afterWhereSql = afterWhereSql.replaceAll("\\b" + column + "\\b", column + COLUMN_SUFFIX);
                flag = true;
            }
        }
        if (!flag) {
            return invocation.proceed();
        }

        String newSql = boundSqlSql.substring(0, whereIndex) + afterWhereSql;
        Map<String, Object> additionalParameters = modifyQueryParam(configuration, boundSql, allIndexSet);

        BoundSql bs = new BoundSql(mappedStatement.getConfiguration(), newSql, boundSql.getParameterMappings(), parameter);
        additionalParameters.forEach((k, v) -> {
            bs.setAdditionalParameter(k, v);
        });
        MappedStatement newMs = copyMappedStatement(mappedStatement, new BoundSqlSqlSource(bs));
        args[0] = newMs;
        return invocation.proceed();
    }

    private Set<String> getAllQueryColumns(List<String> allTableNameList, Map<String, List<String>> queryEncryptColumnsMap) {
        Set<String> set = new HashSet<>();
        for (String tableName : allTableNameList) {
            List<String> list = queryEncryptColumnsMap.get(tableName);
            set.addAll(list);
        }
        return set;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    private MappedStatement copyMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            builder.keyProperty(String.join(",", ms.getKeyProperties()));
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    public static class BoundSqlSqlSource implements SqlSource {
        private BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql) {
            this.boundSql = boundSql;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            return boundSql;
        }
    }

    private Map<String, Object> modifyQueryParam(Configuration configuration, BoundSql boundSql, Set<Integer> allIndexSet) {
        Map<String, Object> additionalParameters = new HashMap<>();
        int index = -1;
        Object sqlParameter = boundSql.getParameterObject();
//        if (sqlParameter instanceof Map) {
//            Map map = (Map) sqlParameter;
//            for (Object key : map.keySet()) {
//                log.info("key:{},value:{}", key, JSON.toJSONString(map.get(key)));
//            }
//        }
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && sqlParameter != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(sqlParameter.getClass())) {
                index++;
                if (allIndexSet.contains(index)) {
                    //todo 需要修改sqlParameter 改动 boundSql
                }
            } else {
                MetaObject metaObject = configuration.newMetaObject(sqlParameter);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        index++;
                        if (allIndexSet.contains(index)) {
                            metaObject.setValue(propertyName, hashCode(obj));
                        }
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        index++;
//                        if (allIndexSet.contains(index)) {
//                            boundSql.setAdditionalParameter(propertyName, hashCode(obj));
//                        }
                        if (allIndexSet.contains(index)) {
                            additionalParameters.put(propertyName, hashCode(obj));
                        } else {
                            additionalParameters.put(propertyName, obj);
                        }
                    }
                }
            }
        }
        return additionalParameters;
    }

    private Object hashCode(Object content) {
        if (content instanceof String) {
            String str = (String) content;
            if (StringUtils.isBlank(str)) {
                return content;
            }
            if (str.length() == 64) {
                return content;
            }
            if (str.startsWith("%") || str.endsWith("%")) {
                boolean startLike = false;
                boolean endLike = false;
                StringBuilder builder = new StringBuilder();
                if (str.endsWith("%")) {
                    endLike = true;
                    builder.deleteCharAt(str.length() - 1);
                }
                if (str.startsWith("%")) {
                    startLike = true;
                    builder.deleteCharAt(0);
                }
                String hashCode = toHashCode(builder.toString());
                return (startLike ? "%" : "") + hashCode + (endLike ? "%" : "");
            } else {
                String hashCode = toHashCode(str);
                return hashCode;
            }
        }
        return content;
    }

    public String toHashCode(String content){
        return content;
    }

    private void findParameterPosition(String afterWhereSql, List<String> queryColumn, List<Integer> indexList) {
        String[] split = afterWhereSql.split("\\?");
        for (int i = 0; i < split.length; i++) {
            for (String column : queryColumn) {
                if (split[i].contains(column)) {
                    indexList.add(i);
                }
            }
        }
    }

    @Deprecated
    public int findParameterPosition(String sql, String fieldName) {
        String regex = fieldName + "\\s*=\\s*\\?";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(sql);

        if (matcher.find()) {
            String subSql = sql.substring(matcher.end());
            int position = 0;
            for (int i = 0; i < matcher.start(); i++) {
                if (sql.charAt(i) == '?') {
                    position++;
                }
            }
            return position;
        }
        return -1;
    }

    public String extractMainTableName(String sql) {
        String regex = "(?i)from\\s+([a-zA-Z_][a-zA-Z0-9_]*)";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(sql);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return "";
    }

    private void findOtherParameterIndex(String afterWhereSql, Set<String> queryColumn, Set<Integer> indexSet) {
        String withoutGroupSql = afterWhereSql;
        String regex = "(?i)\\bGROUP\\b";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(withoutGroupSql);
        if (matcher.find()) {
            withoutGroupSql = withoutGroupSql.replaceAll(regex, " GROUP ");
            int indexOf = withoutGroupSql.lastIndexOf(" GROUP ");
            withoutGroupSql = withoutGroupSql.substring(0, indexOf);
        }
        String[] split = withoutGroupSql.split("\\?");
        for (int i = 0; i < split.length; i++) {
            for (String column : queryColumn) {
                if (split[i].contains(column)) {
                    indexSet.add(i);
                }
            }
        }
    }

    private Set<Integer> getInParameterSqlIndex(String sql, String fieldName) {
//        String regex = "\\b" + fieldName + "\\s+IN\\s*\\(\\s*\\?\\s*(,\\s*\\?\\s*)*\\)";
        String regex = "\\b" + fieldName + "\\s+(?i)IN\\s*\\(\\s*\\?\\s*(,\\s*\\?\\s*)*\\)";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(sql);

        Set<Integer> indices = new HashSet<>();
        if (matcher.find()) {
            String matchedClause = matcher.group();
            System.out.println(matchedClause);
            int fromIndex = sql.indexOf(matchedClause);
            int endIndex = fromIndex + matchedClause.length();

            int currentQuestionMarkIndex = -1;
            List<Integer> allQuestionMarkIndices = new ArrayList<>();
            for (int i = 0; i < sql.length(); i++) {
                if (sql.charAt(i) == '?') {
                    allQuestionMarkIndices.add(currentQuestionMarkIndex++);
                    if (i > fromIndex && i < endIndex) {
                        indices.add(currentQuestionMarkIndex);
                    }
                }
            }
        }
        return indices;
    }

}
